{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "b79faa3a",
   "metadata": {},
   "source": [
    "### A Set of Basic Functions in Computational Music Analysis\n",
    "#### Code written by Lizhou Wang (王力舟), December 2024, GitHub: Lizhou-Wang\n",
    "#### (Music Theory Department, Jacobs School of Music, Indiana University)\n",
    "#### The file includes:\n",
    "- ##### Normal-form and prime-form generator.\n",
    "- ##### Interval-class-vector calculator.\n",
    "- ##### Maximal-evenness analyzer.\n",
    "- ##### Scalar complexity analyzer.\n",
    "- ##### Voice-leading- and Euclidean-distance calculator."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "adbf56f9",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "([6, 7, 9, 11, 0, 2, 3], [0, 1, 3, 4, 6, 8, 9]) ([0, 1, 3, 4, 6, 8, 9], [3, 3, 5, 4, 4, 2]) (False, False) ([6, 7, 9, 11, 0, 2, 3], [True, 22], {'1/2': [[6], [0, 2, 3, 4]], '2/3': [[1, 5, 6], [3]], '3/4': [[4, 6], [0, 2]], '4/5': [[6], [0, 1, 3]], '5/6': [[2, 4, 5, 6], [0]]}, [False, 0], {}) ([0, 1, 4, 5, 7, 9, 10], [0, 2, 3, 5, 7, 9, 10], 2, 1.414)\n"
     ]
    }
   ],
   "source": [
    "# A Comprehensive case test:\n",
    "test = [11,0,2,3,6,7,9] \n",
    "# Shostakovich's characteristic Phrygian mode with scale degrees 4 lowered.\n",
    "\n",
    "normal_prime = normal_prime_form(test)\n",
    "# The normal form is [6,7,9,11,0,2,3] and the prime form is[0,1,3,4,6,8,9].\n",
    "\n",
    "vector = ic_vector(test)\n",
    "# The interval-class vector is [3,3,5,4,4,2].\n",
    "\n",
    "max_even = maximal_even(test)\n",
    "# It is not maximally even in total chromatic;\n",
    "# and it does not fulfill Myhill's property.\n",
    "\n",
    "complexity = detect_complexity(test, True)\n",
    "# It contains 22 ambiguous intervals and 0 contradiction.\n",
    "\n",
    "distance = distance_vl_gm(test, False, 'diatonic')\n",
    "# The minimal distance from this scalar structure to a diatonic scale is\n",
    "# 2 semitones in terms of voice-leading distance and 1.414 \"semitones\"\n",
    "# in terms of Euclidean distance in a 7-dimensional space.\n",
    "\n",
    "print(normal_prime, vector, max_even, complexity, distance)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "19ee8df2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Calculate the normal and prime forms of a given set.\n",
    "\n",
    "# Input: a pitch set in pitch-class or MIDI-pitch\n",
    "# numbers; it allows repetition of pitches or pitch classes.\n",
    "\n",
    "# Output: normal and prime forms of the input set.\n",
    "\n",
    "def normal_prime_form (pitch_set):\n",
    "\n",
    "    # Extract pitch classes, eliminate redundancy,\n",
    "    # and arrange the result in ascending order.\n",
    "    num = len(pitch_set)\n",
    "    for i in range(num):\n",
    "        pitch_set[i] = pitch_set[i]%12\n",
    "    pc_set = list(set(pitch_set))\n",
    "    pc_set.sort()\n",
    "    \n",
    "    # If the pitch-class set has only one item,\n",
    "    # return the result and end the function.\n",
    "    if len(pc_set) == 1:\n",
    "        return [pc_set[0]], [0]\n",
    "    \n",
    "    # Find and store the most compact permutation(s).\n",
    "    cardinality = len(pc_set)\n",
    "    gap_list = []\n",
    "    n = 0\n",
    "    while n < cardinality:\n",
    "        gap_list.append(\n",
    "            (pc_set[n]-pc_set[n-1])%12)\n",
    "        n += 1\n",
    "    gap_max = max(gap_list)\n",
    "    index = []\n",
    "    for i, j in enumerate(gap_list):\n",
    "        if j == gap_max:\n",
    "            index.append(i)\n",
    "    tight = []\n",
    "    for i in index:\n",
    "        tight.append(pc_set[i:]+pc_set[:i])\n",
    "    \n",
    "    # Check each of above permutations. \n",
    "    # If smaller intervals occur towards the upper\n",
    "    # extreme, reverse the order of the permutation.\n",
    "    for item in tight:\n",
    "        for i in range(cardinality-1):\n",
    "            head = (item[i+1]-item[i])%12\n",
    "            tail = (item[-(i+1)]-item[-(i+2)])%12\n",
    "            if head < tail:\n",
    "                break\n",
    "            elif tail < head:\n",
    "                item.reverse()\n",
    "                break\n",
    "            else:\n",
    "                continue\n",
    "    \n",
    "    # Among above permutations, compare their intervals at \n",
    "    # corresponding positions. The first one(s) featuring\n",
    "    # a smaller interval win(s).\n",
    "    # If several permutations win, pick up the first one.\n",
    "    for m in range(cardinality-1):\n",
    "        gap_list = []\n",
    "        for n in range(len(tight)):\n",
    "            gap = min([\n",
    "                (tight[n][m+1]-tight[n][m])%12, (\n",
    "                    tight[n][m]-tight[n][m+1])%12])\n",
    "            gap_list.append(gap)\n",
    "        gap_min = min(gap_list)\n",
    "        if gap_list.count(gap_min) == 1:\n",
    "            normal = tight[gap_list.index(gap_min)]\n",
    "            break\n",
    "        elif gap_min == max(gap_list):\n",
    "            normal = tight\n",
    "            continue\n",
    "        else:\n",
    "            remain = []\n",
    "            for i, j in enumerate(gap_list):\n",
    "                if j == gap_min:\n",
    "                    remain.append(tight[i])\n",
    "            tight = remain\n",
    "    if type(normal[0]) == list:\n",
    "        normal = normal[0]\n",
    "    result_normal = []\n",
    "    \n",
    "    # Check if the winner is in descending order; if so,\n",
    "    # reverse it into ascent. The normal form is found.\n",
    "    for pc in normal:\n",
    "        result_normal.append(pc)\n",
    "    pci = (result_normal[1]-result_normal[0])%12\n",
    "    if pci > 6:\n",
    "        result_normal.reverse()\n",
    "    \n",
    "    # Transpose the winner, making it start with PC-0.\n",
    "    # If it is in descending order, reverse it around\n",
    "    # the axis PC=0. The prime form is found.\n",
    "    count = normal[0]\n",
    "    for i in range(len(normal)):\n",
    "        normal[i] = (normal[i]-count)%12\n",
    "    if normal[1] > 6:\n",
    "        for i in range(1,len(normal)):\n",
    "            normal[i] = (0-normal[i])%12\n",
    "    result_prime = normal\n",
    "    \n",
    "    return result_normal, result_prime\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "c2839049",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "([1, 3, 6, 7], [0, 1, 4, 6])\n",
      "([2, 6, 10], [0, 4, 8])\n",
      "([0, 6], [0, 6])\n",
      "([11, 0, 2, 5, 7], [0, 1, 3, 6, 8])\n",
      "([1, 3, 7, 9], [0, 2, 6, 8])\n",
      "([8, 11, 2, 4], [0, 2, 5, 8])\n",
      "([0, 1, 3, 4, 6, 7, 8, 9, 10], [0, 1, 2, 3, 4, 6, 7, 9, 10])\n",
      "([4, 6, 8, 10, 0, 1], [0, 1, 3, 5, 7, 9])\n",
      "([7, 9, 11, 0, 2, 4, 5], [0, 1, 3, 5, 6, 8, 10])\n"
     ]
    }
   ],
   "source": [
    "# Test above function:\n",
    "for i in [\n",
    "    [1,3,6,7],\n",
    "    [2,10,6],\n",
    "    [60,54],\n",
    "    [7,0,2,11,2,5],\n",
    "    [9,7,13,15,3],\n",
    "    [4,8,2,11],\n",
    "    [0,10,9,7,6,4,3,1,8],\n",
    "    [0,6,10,16,20,25],\n",
    "    [9,5,7,4,0,2,-1]\n",
    "    ]:\n",
    "    print(normal_prime_form(i))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "6ea70991",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Calculate the interval-class vector (IC content) of \n",
    "# the given pitch/pitch-class set.\n",
    "\n",
    "# Input: a pitch set in pitch-class or MIDI-pitch\n",
    "# numbers; it allows repetition of pitches or pitch classes.\n",
    "\n",
    "# Output: (1) the prime form of the input set; and\n",
    "# (2) the interval-class vector of the input set.\n",
    "\n",
    "def ic_vector (pitch_set):\n",
    "    \n",
    "    # Find the prime form of the input; and\n",
    "    # create a null vector for later use.\n",
    "    prime = normal_prime_form(pitch_set)[1]\n",
    "    vector = [0,0,0,0,0,0]\n",
    "    count = len(prime)\n",
    "    \n",
    "    # Calculate and record the interval classes of \n",
    "    # all intervals in the prime form.\n",
    "    for i in range(len(prime)-1):\n",
    "        for j in range(1, count):\n",
    "            upci = prime[-j]-prime[i]\n",
    "            if upci > 6:\n",
    "                upci = 12-upci\n",
    "            vector[upci-1] += 1\n",
    "        count -= 1\n",
    "    \n",
    "    return prime, vector\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "d9f6580d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "([0, 1, 4, 6], [1, 1, 1, 1, 1, 1])\n",
      "([0, 4, 8], [0, 0, 0, 3, 0, 0])\n",
      "([0, 6], [0, 0, 0, 0, 0, 1])\n",
      "([0, 2, 5, 8], [0, 1, 2, 1, 1, 1])\n",
      "([0, 2, 6, 8], [0, 2, 0, 2, 0, 2])\n",
      "([0, 1, 3, 4, 6, 7, 9, 10], [4, 4, 8, 4, 4, 4])\n",
      "([0, 1, 2, 3, 4, 6, 7, 9, 10], [6, 6, 8, 6, 6, 4])\n",
      "([0, 1, 3, 5, 7, 9], [1, 4, 2, 4, 2, 2])\n",
      "([0, 1, 3, 5, 6, 8, 10], [2, 5, 4, 3, 6, 1])\n"
     ]
    }
   ],
   "source": [
    "# Test above function:\n",
    "for i in [\n",
    "    [1,3,6,7],\n",
    "    [2,10,6],\n",
    "    [60,54],\n",
    "    [7,2,11,2,5],\n",
    "    [9,7,13,15,3],\n",
    "    [0,1,3,4,6,7,9,10],\n",
    "    [0,10,9,7,6,4,3,1,8],\n",
    "    [0,6,10,16,20,25],\n",
    "    [9,5,7,4,0,2,-1]\n",
    "    ]:\n",
    "    print(ic_vector(i))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "6e76f5c1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# This is a helper function to calculate and store all\n",
    "# intervals among members of the given set.\n",
    "\n",
    "# Input: a pitch-class set, expected to be an ascending \n",
    "# normal or prime form. The function does not change the\n",
    "# input in anyway (reordering, normal/prime forms, etc.).\n",
    "\n",
    "# Output: the matrix indicating the intervals between \n",
    "# all pairs of different set members.\n",
    "\n",
    "def interval_matrix (pitch_set):\n",
    "    \n",
    "    # Create a matrix showing all intervals among the \n",
    "    # set members; calculated in semitones.\n",
    "    cdt = len(pitch_set)\n",
    "    matrix = [[0]*cdt for _ in range(cdt)]\n",
    "    for i in range(cdt):\n",
    "        pre = pitch_set[i]\n",
    "        for j in range(cdt):\n",
    "            post = pitch_set[j]\n",
    "            interval = (post-pre)%12\n",
    "            matrix[i][j] = interval\n",
    "    \n",
    "    # Extract and store the intervals between all pairs\n",
    "    # of different set members. The results are organized \n",
    "    # according to generic intervals; in each low-level\n",
    "    # list, the indices indicate starting set members.\n",
    "    chrom_matrix = []\n",
    "    for i in range(1, cdt):\n",
    "        chrom = []\n",
    "        for j in range(cdt):\n",
    "            interval = matrix[j][(i+j)%(cdt)]\n",
    "            chrom.append(interval)\n",
    "        chrom_matrix.append(chrom)\n",
    "        \n",
    "    return chrom_matrix\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "d7f317c0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[[3, 3, 3, 3], [6, 6, 6, 6], [9, 9, 9, 9]]\n",
      "[[5, 3, 1, 3], [8, 4, 4, 8], [9, 7, 9, 11]]\n",
      "[[2, 2, 1, 2, 2, 2, 1], [4, 3, 3, 4, 4, 3, 3], [5, 5, 5, 6, 5, 5, 5], [7, 7, 7, 7, 7, 7, 6], [9, 9, 8, 9, 9, 8, 8], [11, 10, 10, 11, 10, 10, 10]]\n",
      "[[2, 2, 3, 2, 3], [4, 5, 5, 5, 5], [7, 7, 8, 7, 7], [9, 10, 10, 9, 10]]\n"
     ]
    }
   ],
   "source": [
    "# Test above function:\n",
    "for i in [\n",
    "    [0,3,6,9], # Diminished seventh\n",
    "    [7,0,3,4], # Major-minor triad\n",
    "    [2,4,6,7,9,11,1], # Major scale\n",
    "    [5,7,9,0,2] # Pentatonic scale\n",
    "    ]:\n",
    "    print(interval_matrix(i))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "92171222",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Use Clough-Douthett theorem to check if the given set is\n",
    "# maximally even in the total chromatic. The theorem states\n",
    "# that a pc set is maximally even if and only if every generic\n",
    "# interval is realized either in a single specific interval\n",
    "# or in two specific intervals in consecutive sizes.\n",
    "# Moreover, it checks if the set fulfills Myhill's property,\n",
    "# which means that each diatonic interval is realized in two\n",
    "# specific intervals in consecutive sizes.\n",
    "\n",
    "# Input: a pitch set in pitch-class or MIDI-pitch\n",
    "# numbers; it allows repetition of pitches or pitch classes.\n",
    "\n",
    "# Output: Two boolean results indicating whether the input set\n",
    "# is maximally even and fulfills Myhill's property.\n",
    "\n",
    "def maximal_even (pitch_set):\n",
    "    \n",
    "    # Get the prime form of the given set.\n",
    "    prime = normal_prime_form(pitch_set)[1]\n",
    "    \n",
    "    # Get the matrix indicating the intervals between all\n",
    "    # pairs of different prime-form members.\n",
    "    chrom_matrix = interval_matrix(prime)\n",
    "    \n",
    "    # Test the evenness using Clough and Douthett's theorem;\n",
    "    # simultaneously, check the Myhill's property.\n",
    "    maximal_even = True\n",
    "    myhill = True\n",
    "    cdt = len(prime)\n",
    "    for i in range(cdt-1):\n",
    "        chrom = chrom_matrix[i]\n",
    "        j = min(chrom)\n",
    "        k = max(chrom)\n",
    "        if j == k-1:\n",
    "            continue\n",
    "        elif j == k:\n",
    "            myhill = False\n",
    "            continue\n",
    "        elif j < k-1:\n",
    "            maximal_even = False\n",
    "            myhill = False\n",
    "            break\n",
    "    \n",
    "    return maximal_even, myhill\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "526af876",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(True, False)\n",
      "(False, False)\n",
      "(True, True)\n",
      "(True, True)\n",
      "(False, False)\n",
      "(False, False)\n"
     ]
    }
   ],
   "source": [
    "# Test above function:\n",
    "for i in [\n",
    "    [0,3,6,9], # Diminished seventh\n",
    "    [7,0,3,4], # Major-minor triad\n",
    "    [7,9,11,2,4], # pentatonic scale\n",
    "    [2,4,6,7,9,11,1], # Major scale\n",
    "    [1,4,5,8,9,12], # Hexatonic scale\n",
    "    [11,0,2,3,6,7,9,10] # Shostakovich's Phrygian-b4/8 scale\n",
    "    ]:\n",
    "    print(maximal_even(i))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "3c0fbae5",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Detect two types of scalar complexity:\n",
    "# (1) Ambiguity: two intervals with consecutive generic \n",
    "# (diatonic) intervals have the same chromatic distance.\n",
    "# (2) Contradiction: between two pc pairs with consecutive\n",
    "# generic (diatonic) intervals, the diatonically smaller one \n",
    "# has larger specific interval.\n",
    "\n",
    "# Input: (1) a pitch set in pitch-class or MIDI-pitch\n",
    "# numbers; it allows repetition of pitches or pitch classes.\n",
    "# (2) Whether it is transformed into normal form firstly.\n",
    "\n",
    "# Output: (1) the normal form, if asked to transform; or \n",
    "# the input set itself. (2) Whether ambiguity is \n",
    "# observed; and the number of cases. (3) All cases of \n",
    "# ambiguity, in the format of {generic interval: locations}.\n",
    "# (4) Whether contradiction is observed; and the number of \n",
    "# cases. (5) All cases of contradiction, in the format of \n",
    "# {generic interval: locations}.\n",
    "\n",
    "def detect_complexity (pitch_set, normalize):\n",
    "    \n",
    "    # If normalize is True, transform the input set into \n",
    "    # the normal form; if not, use the given set directly.\n",
    "    if normalize == True:\n",
    "        normal = normal_prime_form(pitch_set)[0]\n",
    "    else:\n",
    "        normal = pitch_set\n",
    "    \n",
    "    # Get the matrix indicating chromatic intervals between\n",
    "    # all pairs of different set members.\n",
    "    chrom_matrix = interval_matrix(normal)\n",
    "    \n",
    "    # Use above matrix to find ambiguity and contradiction;\n",
    "    # record their generic intervals and locations.\n",
    "    cdt = len(normal)\n",
    "    ambiguity = False\n",
    "    ambgt_case = {}\n",
    "    ambgt_count = 0\n",
    "    contradiction = False\n",
    "    contd_case = {}\n",
    "    contd_count = 0\n",
    "    # In each pair of consecutive generic intervals:\n",
    "    for i in range(cdt-2):\n",
    "        pre = chrom_matrix[i]\n",
    "        pre_max = max(pre)\n",
    "        post = chrom_matrix[i+1]\n",
    "        post_min = min(post)\n",
    "        \n",
    "        # The condition means the existence of complexity.\n",
    "        if pre_max >= post_min:\n",
    "            label = str(i+1)+'/'+str(i+2)\n",
    "            loc_con = []\n",
    "            loc_con_pre = []\n",
    "            loc_con_post = []\n",
    "            loc_amb = []\n",
    "            loc_amb_pre = []\n",
    "            loc_amb_post = []\n",
    "            # Check every case of the smaller generic interval.\n",
    "            # If it chromatically equals/is larger than the\n",
    "            # minimum of the larger generic interval, a case of\n",
    "            # ambiguity/complexity is recorded.\n",
    "            for m, n in enumerate(pre):\n",
    "                if n > post_min:\n",
    "                    loc_con_pre.append(m)\n",
    "                    contd_count += 1\n",
    "                elif n == post_min:\n",
    "                    loc_amb_pre.append(m)\n",
    "                    ambgt_count += 1\n",
    "            if len(loc_con_pre) > 0:\n",
    "                contradiction = True\n",
    "                loc_con.append(loc_con_pre)\n",
    "            if len(loc_amb_pre) > 0:\n",
    "                ambiguity = True\n",
    "                loc_amb.append(loc_amb_pre)\n",
    "            # Check every case of the larger generic interval.\n",
    "            # If it chromatically equals/is smaller than the\n",
    "            # maximum of the smaller generic interval, a case of\n",
    "            # ambiguity/complexity is recorded.\n",
    "            for m, n in enumerate(post):\n",
    "                if n < pre_max:\n",
    "                    loc_con_post.append(m)\n",
    "                    contd_count += 1\n",
    "                elif n == pre_max:\n",
    "                    loc_amb_post.append(m)\n",
    "                    ambgt_count += 1\n",
    "            if len(loc_con_post) > 0:\n",
    "                contradiction = True\n",
    "                loc_con.append(loc_con_post)\n",
    "            if len(loc_amb_post) > 0:\n",
    "                ambiguity = True\n",
    "                loc_amb.append(loc_amb_post)\n",
    "                \n",
    "            # Collect all recorded cases of complexity.\n",
    "            if len(loc_con) > 0:\n",
    "                contd_case.update({label:loc_con})\n",
    "            if len(loc_amb) > 0:\n",
    "                ambgt_case.update({label:loc_amb})\n",
    "        \n",
    "    return normal, [ambiguity, ambgt_count], ambgt_case, [\n",
    "        contradiction, contd_count], contd_case\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "3f575497",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "([4, 7, 10, 0], [False, 0], {}, [False, 0], {})\n",
      "([2, 4, 6, 9, 11], [False, 0], {}, [False, 0], {})\n",
      "([0, 3, 4, 7], [False, 0], {}, [True, 6], {'1/2': [[3], [0, 1]], '2/3': [[2, 3], [0]]})\n",
      "([1, 2, 4, 6, 7, 9, 11], [True, 2], {'3/4': [[4], [0]]}, [False, 0], {})\n",
      "([6, 7, 8, 10, 0, 2, 3], [True, 24], {'1/2': [[2, 3, 4], [1, 4]], '2/3': [[2, 3, 5, 6], [0]], '3/4': [[2, 4], [0, 5]], '4/5': [[3], [0, 1, 4, 5]], '5/6': [[3, 6], [3, 4, 5]]}, [True, 4], {'1/2': [[6], [0]], '5/6': [[2], [0]]})\n"
     ]
    }
   ],
   "source": [
    "# Test above function:\n",
    "for i in [\n",
    "    [0,4,7,10], # Dominant-seventh\n",
    "    [2,4,6,9,11], # Pentatonic scale\n",
    "    [7,0,3,4], # Major-minor triad\n",
    "    [2,4,6,7,9,11,1], # Major scale\n",
    "    [7,8,10,0,2,3,6] # Harmonic Phrygian scale\n",
    "    ]:\n",
    "    print(detect_complexity(i, True))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "308c4291",
   "metadata": {},
   "outputs": [],
   "source": [
    "# (May be a helper function.) Find out the optimal ascending\n",
    "# ordering of a given set, which yields the smallest difference\n",
    "# (voice-leading distance) between it and the referential set.\n",
    "# When \"eucld\" is True, the function may also provide the \n",
    "# Euclidean distance between the optimal choice and the reference.\n",
    "\n",
    "# Input: (1) the tested pc set, which will be firstly sorted. \n",
    "# (2) The referential set, which will not be sorted. (3) Whether\n",
    "# the Euclidean distance is calculated.\n",
    "\n",
    "# Output: (1) the optimal order. (2) Its voice-leading \n",
    "# distance to the reference. (3) Its Euclidean distance to the\n",
    "# reference; if \"eucld\" is False, None is returned.\n",
    "\n",
    "def optimal_order (lst_ps, lst_ref, eucld):\n",
    "    \n",
    "    lst_ps.sort()\n",
    "    cdt = len(lst_ps)\n",
    "    order_list = []\n",
    "    order_vl_dist = []\n",
    "    \n",
    "    # Check all ascending orderings to find the one yielding\n",
    "    # smallest voice-leading distance to the reference.\n",
    "    count = 0\n",
    "    while count < cdt:\n",
    "        reorder = []\n",
    "        reorder.extend(lst_ps[-count:])\n",
    "        reorder.extend(lst_ps[:-count])\n",
    "        vl_distance = 0\n",
    "        for i in range(cdt):\n",
    "            j = reorder[i]\n",
    "            k = lst_ref[i]\n",
    "            gap = min([(k-j)%12,(j-k)%12])\n",
    "            vl_distance += gap\n",
    "        order_list.append(reorder)\n",
    "        order_vl_dist.append(vl_distance)\n",
    "        count += 1\n",
    "    optimal_dist = min(order_vl_dist)\n",
    "    optimal_index = order_vl_dist.index(optimal_dist)\n",
    "    optimal_order = order_list[optimal_index]\n",
    "    optimal_dist = round(optimal_dist, 3)\n",
    "    \n",
    "    # If asked, calculate the Euclidean distance between the\n",
    "    # selected optimal ordering and the reference.\n",
    "    if eucld == True:\n",
    "        eucld_count = 0\n",
    "        for i in range(cdt):\n",
    "            j = optimal_order[i]\n",
    "            k = lst_ref[i]\n",
    "            gap = min([(k-j)%12,(j-k)%12])\n",
    "            eucld_count += gap**2\n",
    "        optimal_eucld = round(eucld_count**0.5, 3)\n",
    "    else:\n",
    "        optimal_eucld = None\n",
    "    \n",
    "    return optimal_order, optimal_dist, optimal_eucld\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "7a233855",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "([1, 9], 4, 3.162)\n",
      "([11, 1, 4, 7], 5, 2.646)\n",
      "([10, 2, 3, 7, 9], 5, 3.0)\n",
      "([1, 2, 4, 5, 8, 9, 11], 4, 2.0)\n"
     ]
    }
   ],
   "source": [
    "# Test above function:\n",
    "for i,j in [\n",
    "    [[9,1],[0,6]],# a pair of intervals.\n",
    "    [[4,7,11,1],[0,2,6,8]],# C# Half-diminished vs. D French-sixth.\n",
    "    [[7,9,10,2,3],[0,2,5,7,10]],# G Japanese mode vs. Bb pentatonic.\n",
    "    [[9,11,2,1,5,8,4],[0,2,3,5,7,9,10]]# A harmonic major vs. Bb major.\n",
    "    ]:\n",
    "    print(optimal_order(i, j, True))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "4271db77",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Calculate two types of distance between a tested structure\n",
    "# and a referential structure. The tested \"pitch_set\" allows\n",
    "# transpositional and permutational transformation, but not\n",
    "# inversional transformation. Thus, the function compares the\n",
    "# two abstract \"structures,\" rather than specific pc content.\n",
    "# The two distance types are (1) Voice-leading distance, which\n",
    "# means how many semitones it needs to move from one set to\n",
    "# another; and (2) Euclidean distance, which means the geometric\n",
    "# distance between two points representing the two sets in \n",
    "# a space in dimensions of the cardinality of the two sets.\n",
    "\n",
    "# Input: (1) a pitch set in pitch-class or MIDI-pitch\n",
    "# numbers; it allows repetition of pitches or pitc classes.\n",
    "# (2) Whether a perfectly even scale is used as the\n",
    "# referential structure; it may lead to microtonal positions;\n",
    "# if this variable is set to True, 'reference' should be None.\n",
    "# (3) If perft_even is set to False, a scalar structure\n",
    "# is assigned as the reference, either a manually created list\n",
    "# or a string calling a reference in \"ref_dict\" dictionary.\n",
    "\n",
    "# Output: (1) The voice-leading distance between the tested\n",
    "# structure and the reference. (2) The Euclidean distance\n",
    "# between the two structure.\n",
    "\n",
    "def distance_vl_gm (pitch_set, perft_even, reference):\n",
    "    \n",
    "    # Extract pitch classes, eliminate redundancy,\n",
    "    # and arrange the result in ascending order.\n",
    "    num = len(pitch_set)\n",
    "    for i in range(num):\n",
    "        pitch_set[i] = pitch_set[i]%12\n",
    "    pc_set = list(set(pitch_set))\n",
    "    cdt = len(pc_set)\n",
    "    if cdt == 1:\n",
    "        print('Trivial case: single pitch class.')\n",
    "        return None\n",
    "    \n",
    "    # Create the referential scale. If perft_even is True,\n",
    "    # the octave is devided equally (and microtonally, when\n",
    "    # necessary) according to the cardinality of the pc set.\n",
    "    if perft_even == True:\n",
    "        unit = 12/cdt\n",
    "        ref = []\n",
    "        for i in range(cdt):\n",
    "            ref.append(round(unit*i, 4))\n",
    "        if cdt % 2 == 0:\n",
    "            sum_class_ref = 6\n",
    "        else:\n",
    "            sum_class_ref = 0\n",
    "    # If perft_even is False, the 'reference' variable is\n",
    "    # checked. If it's a list, it's taken as the reference;\n",
    "    # if it's a string, corresponding value in ref_dict is\n",
    "    # selected as the reference.\n",
    "    else:\n",
    "        if type(reference) == list:\n",
    "            reference.sort()\n",
    "            ref = reference\n",
    "            sum_class_ref = sum(ref)%12\n",
    "        else:\n",
    "            # Each value includes the pc set and its sum class.\n",
    "            ref_dict = {\n",
    "                'pentatonic':[[0,2,5,7,10],0],\n",
    "                'hexatonic':[[1,2,5,6,9,10],9],\n",
    "                'mystic-6':[[0,3,4,6,8,10],7],\n",
    "                'diatonic':[[0,2,3,5,7,9,10],0],\n",
    "                'minor_har':[[0,2,3,5,7,8,11],0],\n",
    "                'mystic-7':[[0,2,4,5,7,8,10],0],\n",
    "                'Shost_b4':[[0,2,4,5,6,9,10],0],\n",
    "                'Shost_b24':[[0,1,4,5,7,9,10],0],\n",
    "                'Shost_b245':[[0,2,4,5,7,8,10],0],\n",
    "                'Shost_b248':[[0,2,3,4,5,7,8,11],4],\n",
    "                'Shost_b2458':[[1,2,3,4,6,7,9,11],7],\n",
    "                'octatonic':[[0,1,3,4,6,7,9,10],4],\n",
    "                'enneatonic':[[0,1,3,4,5,7,8,9,11],0],\n",
    "                'Shost_mode':[[0,1,2,4,5,7,8,9,11],11]\n",
    "                }\n",
    "            try:\n",
    "                ref_info = ref_dict.get(reference)\n",
    "            except:\n",
    "                print('Can not find the reference')\n",
    "                return None\n",
    "            ref = ref_info[0]\n",
    "            sum_class_ref = ref_info[1]\n",
    "        if len(ref) != cdt:\n",
    "            print('Cardinality Error')\n",
    "            return None\n",
    "        \n",
    "    # Generate all possible sum-class differences between\n",
    "    # set transpositions and the referential structure.\n",
    "    distance_list = []\n",
    "    sum_class = sum(pc_set) % 12\n",
    "    distance_list.append(\n",
    "        abs(sum_class_ref - sum_class))\n",
    "    new_sum = (sum_class + cdt) % 12\n",
    "    while new_sum != sum_class:\n",
    "        distance_list.append(\n",
    "            abs(sum_class_ref - new_sum))\n",
    "        new_sum = (new_sum + cdt) % 12\n",
    "    \n",
    "    # Then, generate the set transpositions whose sum classes\n",
    "    # are closest to the sum class of the referential scale.\n",
    "    # If the smallest difference is zero, two more choices\n",
    "    # are generated, \"surrounding\" the first one in terms of \n",
    "    # sum class; if not, two are created, surrounding the\n",
    "    # sum class of the reference.\n",
    "    min_index = []\n",
    "    distance_min = min(distance_list)\n",
    "    pick = distance_list.index(distance_min)\n",
    "    min_index.append(pick)\n",
    "    if distance_min == 0:\n",
    "        count = 2\n",
    "    else:\n",
    "        count = 1\n",
    "    while count > 0:\n",
    "        distance_list[pick] = 12\n",
    "        distance_min = min(distance_list)\n",
    "        pick = distance_list.index(distance_min)\n",
    "        min_index.append(pick)\n",
    "        count -= 1\n",
    "    set_list = []\n",
    "    for i in min_index: \n",
    "        set_optimal = [(\n",
    "            x + i) % 12 for x in pc_set]\n",
    "        set_optimal.sort()\n",
    "        set_list.append(set_optimal)\n",
    "    \n",
    "    # Find among above sets the one featuring smallest \n",
    "    # possible voice-leading distance to the reference;\n",
    "    # Return the voice-leading and Euclidean distances.\n",
    "    distance_vl = []\n",
    "    distance_gm = []\n",
    "    order = optimal_order(set_list[0], ref, False)\n",
    "    for i in range(1, len(set_list)):\n",
    "        order_new = optimal_order(set_list[i], ref, False)\n",
    "        if order_new[1] < order[1]:\n",
    "            order = order_new\n",
    "    pc_set = order[0]\n",
    "    distance_vl = round(order[1], 3)      \n",
    "    count_gm = 0\n",
    "    for i in range(cdt):\n",
    "        j = pc_set[i]\n",
    "        k = ref[i]\n",
    "        gap = min([(k-j)%12,(j-k)%12])\n",
    "        count_gm += gap**2\n",
    "    distance_gm = round(count_gm**0.5, 3)\n",
    "    \n",
    "    return pc_set, ref, distance_vl, distance_gm\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "240048ea",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "([3, 4], [0.0, 6.0], 5.0, 3.606)\n",
      "([2, 4, 7, 11], [1, 5, 7, 11], 2, 1.414)\n",
      "([11, 3, 4, 8, 10], [0, 2, 5, 7, 10], 4, 2.0)\n",
      "([0, 2, 3, 5, 7, 9, 10], [0.0, 1.7143, 3.4286, 5.1429, 6.8571, 8.5714, 10.2857], 1.714, 0.756)\n",
      "([0, 1, 2, 4, 5, 6, 8, 9, 10], [0, 1, 2, 4, 5, 7, 8, 9, 11], 2, 1.414)\n"
     ]
    }
   ],
   "source": [
    "# Test above function:\n",
    "for i,j,k in [\n",
    "    [[3,4], True, None],# minor second vs. equal division\n",
    "    [[3,6,10,13], False, [5,11,1,7]],# minor-minor seventh vs. French-sixth\n",
    "    [[9,11,12,4,5], False, 'pentatonic'],# Japanese mode vs. pentatonic\n",
    "    [[5,7,9,10,0,4,2], True, None],# Diatonic scale vs. equal division\n",
    "    [[0,1,2,4,5,6,8,9,10],False,'Shost_mode']# enneatonic vs. Shostakovich mode\n",
    "    ]:\n",
    "    print(distance_vl_gm (i,j,k))"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
